#! /usr/bin/python
""" Symbol Dependencies
Basically, the building blocks of a software systems are their symbols: 
objects, methods, fields, functions and variables.

All this building blocks are usually divided in logical modules:
objects and libraries.

One of the most powerful way to analyse a software system is by looking at the 
symbol interdependencies between its modules.

This python library is providing utilities to parse, store and query all the 
typical symbols information of a software system and their interdependencies.

The basic components of this library are:
  - System: Object containing all the module symbols information and different
    methods to query or output this information.
  - Resource: a data structure containing the data about a software system
    resource (an object file, or a library).
  - Element: a data structure containing all the information about the provided 
    and used symbols and a map of the interdependencies with other elements.  
  - ResourceParser: a function retrieving all the symbol information from the
    resources and generating a list of elements.
    A resource can be mapped to a single element or can be split to multiple
    elements, depending on the desired granularity; ie: a library can be
    considered a single element or can be split in its contained objects.
    A typical ResourceParser can be a simple parser of the output of tools like 
    the "GNU nm".
  
The basic steps to create a System object containing all the linker information 
are:
  - Get a Resource list.
  - Give the Resource list to a ResourceParser to obtain an Element list.
  - Initialise a System object with an Element list.

The System queries and output are:
  - generate_LDI: Output a Lattix Data Import file to generate a Dependency 
    Structure Matrix with Lattix.
  - getProvider: Output the provider of a given symbol.
  - getDependencies: Output the list of symbols that a module is using form
    another module.
  - generateDependencyGraph: Output a graph of the dependencies of a single 
    element of the system
"""
import resource, utility


class System:
    def __init__(self, elements, IGNORE_UNDEFINED=False, IGNORE_CONFLICT=False):
        """ 
        @param elements
            A list of elements returned by an ElementParser.
        @param IGNORE_UNDEFINED
            True if the undefined symbols have to be ignored, False otherwise
        @param IGNORE_CONFLICT
            True if the conflicting symbols have to be ignored, False otherwise
        """
        self.elementList = elements
        self.elementList.sort()
        self.elementMap = {} # <Element.name, Element>
        
        # Map to lookup the provider of a given symbol
        # TODO: Probably it should be better to use a map <Symbol.name: Element[]> To handle the definition of a symbol from
        # more than a module
        # This can happen if a given symbol is provided by a module that can be compiled in different configurations
        # or if the same functionalities can be provided by different libraries
        self.symMap = {} # <Symbol.name: Element>
        symbolsToBeRemoved = []
        for obj in self.elementList:
            self.elementMap[obj.name] = obj
            for symName, sym in obj.provided_symbols.iteritems():
                if not symName in self.symMap:
                    self.symMap[symName] = obj
                    sym.provider = obj.name
                else:
                    if not IGNORE_CONFLICT:
                        competitor = self.symMap[symName]
                        # if the conflict was not resolved it is better to remove also the other provider
                        # it is highly probable that it is a shared symbol that doesn't spot a real dependency between
                        # two elements
                        symbolsToBeRemoved.append(symName)
                        print '[WARNING] Redefinition: ' + symName 
                        print '    [' + sym.type + '] ' + obj.name 
                        print '    [' + sym.type + '] ' + competitor.name
        
        for symName in symbolsToBeRemoved:
            if symName in self.symMap:
                del self.symMap[symName]
        
        # Dependencies
        uid = 1
        for obj in self.elementList:
            obj.uid = uid
            uid += 1
            for usedName, used in obj.used_symbols.iteritems():
                if usedName in self.symMap:
                    provider = self.symMap[usedName]
                    # If an element defines and uses a symbol at the same time, it is not a dependency
                    if provider.name != obj.name:
                        # Add the symbol to the list of the symbols provided by this element
                        if not provider.name in obj.uses_element_symbols:
                            obj.uses_element_symbols[provider.name] = []
                        obj.uses_element_symbols[provider.name].append(usedName)
                else:
                    if not IGNORE_UNDEFINED:
                        print '[WARNING] The symbol (' + used.name + ') is not defined'
            
            for providerName, symbolList in obj.uses_element_symbols.iteritems():
                obj.uses_element_symbols[providerName] = set(symbolList)
    
    def getProvider(self, symName):
        """ Output the provider of a given symbol
        @param symName
            The name of the symbol of whom we want to know the provider
        @return
            The provider of the symName symbol
        """
        provider = self.symMap[symName]
        print "%s is provided by: %s" % (symName, provider.name)
        return provider
    
    def getDependencies(self, dependant, provider):
        """
        @param dependant
            The object depending from the symbols defined in another module
        @param provider
            An object provider of symbols
        @return
            A list of symbol used by dependent and defined by provider
        """
        if not dependant in self.elementMap:
            print 'The element (%s) is not in the elements map of the system' % dependant
            return None
        depElement = self.elementMap[dependant]
        
        if not provider in depElement.uses_element_symbols:
            print 'The element (%s) is not in the providers map of the element (%s)' % (provider, dependant)
            return None
        dependencies = depElement.uses_element_symbols[provider]
        print 'The element (%s) depends on the following symbols defined by (%s):' % (dependant, provider)
        for symName in dependencies:
            print '\t%s' % symName 
        return dependencies
    
    # LDI file template
    LDI_DOCUMENT = '<?xml version="1.0" ?>\n<ldi>\n%(elements)s</ldi>'
    LDI_ELEMENT = '    <element name="%(name)s">\n%(uses)s    </element>\n'
    LDI_USES = '        <uses provider="%(provider)s" strength="%(strength)s"/>\n'
    def generate_LDI(self, filename):
        """ Generates a Lattix Data Import file
        @param filename
            The LDI file name (Note: the '.ldi.xml' extension will be added)
        """
        elements = ''
        for obj in self.elementList:
            uses = ''
            for provider, symbolList in obj.uses_element_symbols.iteritems():
                strength = len(symbolList)
                uses += System.LDI_USES % {"provider": provider, "strength": strength}
            
            elements += System.LDI_ELEMENT % {"name": obj.name, "uses": uses} 
        
        file = open(filename+'.ldi.xml',"w")
        file.write(System.LDI_DOCUMENT % {"elements": elements})
    
    def generate_CSV(self, filename):
        """ Generates a CSV file to be imported by the CAM DSM toolbox
        @param filename
            The CSV file name (Note: the '.csv' extension will be added)
        """
        import csv
        writer = csv.writer(open(filename+'.csv',"w"), quoting=csv.QUOTE_ALL)
        writer.writerow([""] + [e.name for e in self.elementList])
        for e in self.elementList:
            row = [e.name]
            for u in self.elementList:
                if e.name in u.uses_element_symbols:
                    row.append(len(u.uses_element_symbols[e.name]))
                else:
                    row.append("")
            writer.writerow(row)
    
    S101_DOC = """<data flavor="com.headway.examples.halloworld">
    <modules>\n%(modules)s\n    </modules>
    <dependencies>\n%(dependencies)s\n    </dependencies>\n</data>"""
    S101_MODULE     = '         <module name="%(name)s" id="%(id)d" type="object"/>'
    S101_DEPENDENCY = '         <dependency from="%(from)d" to="%(to)d" type="use"/>'
    
    def generate_S101(self, filename):
        """ Generates a Structure101 data file
        @param filename
            The Structure101 file name (Note: the '.xml' extension will be added)
        """
        modules = []
        dependencies = []
        for e in self.elementList:
            modules.append(System.S101_MODULE % {'name': e.name, 'id': e.uid})
            for e_name, _ in e.uses_element_symbols.iteritems():
                d = self.elementMap[e_name]
                dependencies.append(System.S101_DEPENDENCY % {'from':e.uid, 'to':d.uid})
        
        open(filename+'.xml',"w").write(System.S101_DOC % {
            "modules": '\n'.join(modules),
            "dependencies": '\n'.join(dependencies)
        })
    
    def addDepSubset(self, element, depSubset):
        """ A method to recursively generate a system subset with the dependencies of a given element
        @param element
            The element 
        @param depSubset
            The dependency subset map <element.name, dependency.name[]>
        """
        for depName, _ in element.uses_element_symbols.iteritems():
            if not element.name in depSubset:
                depSubset[element.name] = []
            depSubset[element.name] .append(depName)
            
            if not depName in depSubset:
                dep = self.elementMap[depName]
                self.addDepSubset(dep, depSubset)
    
    # Dependency tree template
    GRAPH_MAIN = 'digraph %(graphName)s {\n    size="24,24";\n    node [color=lightblue2, style=filled];\n%(deps)s}'
    GRAPH_DEP  = '    "%(elementName)s" -> "%(depName)s";\n'
    
    def generateDependencyGraph(self, filename, name):
        """ Generate a graph with the dependencies of a given element
        @param filename
            The file name of the graph 
        @param name
            The name of the element within the system
        """
        if not name in self.elementMap:
            print 'The element (%s) is not in the elements map of the system' % name
            return
        element = self.elementMap[name]
        
        # Get a subset of element this element depend from
        depSubset = {} # <element.name, depName[]>
        self.addDepSubset(element, depSubset)
        
        deps = ''
        for elementName, depNames in depSubset.iteritems():
            for depName in depNames:
                deps += System.GRAPH_DEP % {'elementName': elementName, 'depName': depName}
        file = open(filename+'.dot',"w")
        file.write(System.GRAPH_MAIN % {"graphName": name, "deps": deps})
        
        utility.cmd(('dot', '-Tpng', '-o'+filename+'.png', filename+'.dot'))


def genSystem(base, ext, cache=False):
    """
    System generation using the basic resource list generator and the basic 
    resource parser
     @param base
        The base directory to be recursively scanned
    @param ext
        The extensions list of the resources we want to add to the resource list
    @return
        The generated system
    """
    resources = resource.resourceList(base, ext)
    parser = resource.GNUParser()
    elements = parser.parse_resources(resources, cache=cache)
    system = System(elements, IGNORE_UNDEFINED=True)
    return system
